{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "d8bfa2a3",
   "metadata": {},
   "source": [
    "# Introduction\n",
    "This notebook contains Python code snippets that explore and manipulate keystroke data available at [Dataverse URL to come]. Each block of cells is self-contained. All cells are agnostic to which of the two available datasets is used. Simply copy one of the *keystroke.csv*, *students.csv*, and *due.csv* files into this directory."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2a3d66db",
   "metadata": {},
   "source": [
    "## Basic keystroke features\n",
    "These cells read <i>keystrokes.csv</i> and print out basic features of the datasets, like participants, assignments, etc."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "38b00d75",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "df = pd.read_csv('keystrokes.csv')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "de78f01e",
   "metadata": {},
   "outputs": [],
   "source": [
    "df.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6654a644",
   "metadata": {},
   "outputs": [],
   "source": [
    "# print('Participants:', df.SubjectID.unique())\n",
    "print(f'Number of participants: {len(df.SubjectID.unique())}')\n",
    "print('Assignments:', df.AssignmentID.unique())\n",
    "print('Event types:', df.EventType.unique())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e23dec79",
   "metadata": {},
   "source": [
    "## Reconstruct a submission"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8df4b6e4",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "df = pd.read_csv('keystrokes.csv')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "8e66f966",
   "metadata": {},
   "outputs": [],
   "source": [
    "# # Random\n",
    "# sample = df.sample(1)\n",
    "# subject = sample.SubjectID.values[0]\n",
    "# assignment = sample.AssignmentID.values[0]\n",
    "# file = sample.CodeStateSection.values[0]\n",
    "# Specific\n",
    "subject = 'S219'\n",
    "assignment = 'p6s'\n",
    "file = 'tasks.py'\n",
    "\n",
    "# Filter and handle nan\n",
    "f = df[(df.SubjectID == subject)&(df.AssignmentID == assignment)&\n",
    "       (df.CodeStateSection == file)&(df.EventType == 'File.Edit')]\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e9fdcda0",
   "metadata": {},
   "outputs": [],
   "source": [
    "def reconstruct(df):\n",
    "    s = ''\n",
    "    for _,row in df[df.EventType=='File.Edit'].iterrows():\n",
    "        i = int(row.SourceLocation)\n",
    "        insert = '' if pd.isna(row.InsertText) else row.InsertText\n",
    "        delete = '' if pd.isna(row.DeleteText) else row.DeleteText\n",
    "        s = s[:i] + insert + s[i+len(delete):]\n",
    "    return s\n",
    "\n",
    "s = reconstruct(f)\n",
    "print(s)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "969a367d",
   "metadata": {},
   "source": [
    "## Export keystrokes for a single student and then for a single assignment\n",
    "This is useful when using KeystrokeExplorer, which can't handle the entire *keystrokes.csv* file for Dataset1."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2905d4cb",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "df = pd.read_csv('keystrokes.csv')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "caab81b2",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Random\n",
    "sample = df.sample(1)\n",
    "subject = sample.SubjectID.values[0]\n",
    "assignment = sample.AssignmentID.values[0]\n",
    "# Specific\n",
    "subject = 'S018'\n",
    "assignment = 'p4s'\n",
    "\n",
    "df[df.SubjectID == subject].to_csv(f'test-{subject}.csv', index=False)\n",
    "df[df.AssignmentID == assignment].to_csv(f'test-{assignment}.csv', index=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9176b84a",
   "metadata": {},
   "source": [
    "## Histogram of the top 20 keystrokes\n",
    "Because the two datasets log keystrokes slightly differently, the code for the two is a little different"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "id": "c2e64620",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import seaborn as sns\n",
    "import matplotlib.pyplot as plt\n",
    "df = pd.read_csv('keystrokes.csv')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5f0659ab",
   "metadata": {},
   "outputs": [],
   "source": [
    "if ('X-Keystroke' in set(df.columns)):\n",
    "    # Dataset 1 - each keystroke has its own entry\n",
    "    count = df.loc[~df['X-Keystroke'].isna(), 'X-Keystroke'].value_counts()\n",
    "else:\n",
    "    # Dataset 2 - Each keystroke has its own entry. EditType is X-Keystroke\n",
    "    #   for printable characters and X-Action for meta keys.\n",
    "    mask = (df.EventType == 'X-Keystroke')|((df.EventType == 'X-Action')&(~df['X-Metadata'].isna()))\n",
    "    count = df.loc[mask, 'X-Metadata'].value_counts()\n",
    "    \n",
    "top20 = count.iloc[0:20]\n",
    "sns.barplot(x=top20.index, y=top20.values)\n",
    "plt.xticks(rotation = 90);\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8aad02f8",
   "metadata": {},
   "source": [
    "## Estimate how long it took for a random student to complete a random assignment\n",
    "This uses the method given in \"A Practical Model for Student Engagement,\" Edwards et al, SIGCSE 2022."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "de5573d3",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import math\n",
    "import numpy as np\n",
    "\n",
    "df = pd.read_csv('keystrokes.csv')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 60,
   "id": "796602ff",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Given a number of minutes x since the last keystroke,\n",
    "# returns the probability that the student was on task.\n",
    "# Uses the model from Edwards et al, SIGCSE 2022.\n",
    "def p(x):\n",
    "    if x < .75:\n",
    "        m = -(1-0.8072438106027864)/.75\n",
    "        return m*x+1\n",
    "    Q = 6604\n",
    "    B = -4.99\n",
    "    M = 0.01\n",
    "    v = 58.32\n",
    "    return 1 / (1+Q*math.e**(-B*(x-M)))**(1/v)\n",
    "\n",
    "# Given a number of minutes x since the last keystroke,\n",
    "# returns whether the student was on task. Uses p(x)\n",
    "# to get the probability and uses that result to take\n",
    "# a percentage of the time between keystrokes.\n",
    "def on_task(x):\n",
    "    if x <= 0 or x > 60:\n",
    "        return 0\n",
    "    return x * p(x)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 61,
   "id": "103b1785",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Look only at file edits\n",
    "df = df[df.EventType == 'File.Edit']\n",
    "# Compute the elapsed time since the last keystroke\n",
    "df['elapsed'] = df.ClientTimestamp - df.shift(1).ClientTimestamp\n",
    "# Create a unique ID for subject/assignment/file\n",
    "df['ID'] = df.SubjectID + df.AssignmentID + df.CodeStateSection\n",
    "# Any change to a new ID results in 0 elapsed\n",
    "df.loc[df.ID != df.shift(1).ID, 'elapsed'] = 0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f4cb8a64",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Get a random assignment\n",
    "sample = df.sample(1)\n",
    "subject = sample.SubjectID.values[0]\n",
    "assignment = sample.AssignmentID.values[0]\n",
    "single = df[(df.SubjectID == subject)&(df.AssignmentID == assignment)]\n",
    "# Estimate time on task. The on_task function expects minutes\n",
    "# (so divide by 60*1000) and we want to print in hours (so divide\n",
    "# again by 60).\n",
    "time_on_task = ((single.elapsed/(60*1000)).apply(on_task)/60).sum()\n",
    "print(f'Student {subject} took {time_on_task:.2f} hours on assignment {assignment}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9c5b992f",
   "metadata": {},
   "source": [
    "## Compare the median number of edit events between students with exam scores above and below 90\n",
    "It turns out that higher performing students on the exams use more edits to complete their assignments. But maybe they're just more likely to complete their assignments..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2070e2d9",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import seaborn as sns\n",
    "\n",
    "df = pd.read_csv('keystrokes.csv')\n",
    "students = pd.read_csv('students.csv')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 97,
   "id": "a19c625e",
   "metadata": {},
   "outputs": [],
   "source": [
    "above = students[students.exam1 >= 90].SubjectID.unique()\n",
    "below = students[students.exam1 < 90].SubjectID.unique()\n",
    "# Choose the first assignment\n",
    "assignment = df.AssignmentID.values[0]\n",
    "count_above = df.loc[df.SubjectID.isin(above) & (df.AssignmentID == assignment), 'SubjectID'].value_counts()\n",
    "count_below = df.loc[df.SubjectID.isin(below) & (df.AssignmentID == assignment), 'SubjectID'].value_counts()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 98,
   "id": "ec2e8d39",
   "metadata": {},
   "outputs": [],
   "source": [
    "count_above = count_above.to_frame()\n",
    "count_above['Group'] = 'above'\n",
    "count_below = count_below.to_frame()\n",
    "count_below['Group'] = 'below'\n",
    "count = count_above.append(count_below).reset_index()\n",
    "count.columns = ['SubjectID', 'Count', 'Group']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f822584b",
   "metadata": {},
   "outputs": [],
   "source": [
    "display(f'mean above: {count[count.Group==\"above\"].Count.mean()}')\n",
    "display(f'mean below: {count[count.Group==\"below\"].Count.mean()}')\n",
    "sns.displot(x='Count', hue='Group', data=count, kind='kde')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "018d5484",
   "metadata": {},
   "source": [
    "## Scatterplot of number of keystrokes vs score on an assignment"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 114,
   "id": "6f276c14",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import seaborn as sns\n",
    "import scipy.stats\n",
    "\n",
    "df = pd.read_csv('keystrokes.csv')\n",
    "students = pd.read_csv('students.csv')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5efcabf8",
   "metadata": {},
   "outputs": [],
   "source": [
    "# No assignment in particular\n",
    "assignment = df.AssignmentID.values[0]\n",
    "# A specific assignment\n",
    "assignment = 'p6s'\n",
    "\n",
    "count = df.loc[df.AssignmentID == assignment, 'SubjectID'].value_counts()\n",
    "count = count.to_frame().reset_index()\n",
    "count.columns = ['SubjectID', 'NumKeystrokes']\n",
    "count = count.merge(students, on='SubjectID')\n",
    "\n",
    "raw_assignment = assignment\n",
    "if 'X-RawAssignmentID' in df.columns:\n",
    "    # Dataset 1\n",
    "    raw_assignment = assignment[:-1]\n",
    "    count = count[count.Group == ('Fall' if assignment[-1] == 'f' else 'Spring')]\n",
    "count = count[['SubjectID', 'NumKeystrokes', raw_assignment]]\n",
    "count.columns = ['SubjectID', 'NumKeystrokes', 'Score']\n",
    "count = count.dropna()\n",
    "# count\n",
    "\n",
    "sns.scatterplot(x='NumKeystrokes', y='Score', data=count)\n",
    "display(f'Pearson r correlation: {scipy.stats.pearsonr(count.NumKeystrokes, count.Score)}')\n",
    "\n",
    "count[count.Score == 0]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "37fb1153",
   "metadata": {},
   "source": [
    "## Scatterplot of final submission size vs score on an assignment\n",
    "We could reconstruct each file and count the number of characters but a faster way is to get the length of each text insert and deletion and sum those."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5480c5f9",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import seaborn as sns\n",
    "import scipy.stats\n",
    "\n",
    "df = pd.read_csv('keystrokes.csv')\n",
    "students = pd.read_csv('students.csv')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 175,
   "id": "2e5ee741",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Pearson r correlation: (0.33776040207184294, 8.679673761910991e-08)'"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>SubjectID</th>\n",
       "      <th>SubSize</th>\n",
       "      <th>Score</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>108</th>\n",
       "      <td>S219</td>\n",
       "      <td>2527.0</td>\n",
       "      <td>20.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>220</th>\n",
       "      <td>S439</td>\n",
       "      <td>2085.0</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>254</th>\n",
       "      <td>S500</td>\n",
       "      <td>2100.0</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "    SubjectID  SubSize  Score\n",
       "108      S219   2527.0   20.0\n",
       "220      S439   2085.0    0.0\n",
       "254      S500   2100.0    0.0"
      ]
     },
     "execution_count": 175,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# No assignment in particular\n",
    "assignment = df.AssignmentID.values[0]\n",
    "# A specific assignment\n",
    "assignment = 'p6s'\n",
    "\n",
    "mask = (df.AssignmentID == assignment)&(df.EventType == 'File.Edit')\n",
    "df.loc[mask,'Diff'] = df[mask].InsertText.fillna('').str.len() - df[mask].DeleteText.fillna('').str.len()\n",
    "count = df[mask].groupby('SubjectID').agg({'Diff':'sum'}).reset_index()\n",
    "count.columns = ['SubjectID','SubSize']\n",
    "# count.head()\n",
    "\n",
    "count = count.merge(students, on='SubjectID')\n",
    "\n",
    "raw_assignment = assignment\n",
    "if 'X-RawAssignmentID' in df.columns:\n",
    "    # Dataset 1\n",
    "    raw_assignment = assignment[:-1]\n",
    "    count = count[count.Group == ('Fall' if assignment[-1] == 'f' else 'Spring')]\n",
    "count = count[['SubjectID', 'SubSize', raw_assignment]]\n",
    "count.columns = ['SubjectID', 'SubSize', 'Score']\n",
    "count = count.dropna()\n",
    "count\n",
    "\n",
    "sns.scatterplot(x='SubSize', y='Score', data=count)\n",
    "display(f'Pearson r correlation: {scipy.stats.pearsonr(count.SubSize, count.Score)}')\n",
    "\n",
    "count[(count.Score < 30)&(count.SubSize > 2000)]"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
